app_lba_start equ 100                           ;定义一个常数100，这个常数也是用户程序所在的扇区
section mbr align=16 vstart=0x7c00              ;定义mbr段，对齐方式是16，并告诉编译器程序加载的起始地址是0x7c00
mov ax,0                                        
mov ss,ax                                       
mov sp,ax                                       ;设置堆栈段和堆栈指针

mov ax,[cs:phy_base]                            ;把0x10000的低两个字节赋值给ax
mov dx,[cs:phy_base+0x02]                       ;把0x10000的高两个字节赋值给dx
mov bx,16
div bx                                          ;把0x10000除以16，获得用户程序所在的段地址，在ax中
mov ds,ax                                       ;设置了段地址
mov es,ax                                       ;设置了段地址

xor di,di                                       ;将di寄存器设置为0
mov si,app_lba_start                            ;把用户所在扇区100赋值给si寄存器
xor bx,bx                                       ;把bx寄存器赋值为0
call read_hard_disk_0                           ;调用read_hard_disk_0 函数

mov dx,[2]                                      ;读取用户程序长度，先读取高二字节
mov ax,[0]                                      ;读取用户程序长度，后读取高二字节
mov bx,512                                      ;一个扇区512字节，算用户程序占用了多少个扇区
div bx                                          ;算用户程序占用了多少个扇区
cmp dx,0                                        ;检查用户程序的扇区是否完整，dx存放了余数
jnz @1                                          ;dx不为0，代表用户程序最后一段有多的字节，占用了一个扇区，跳转到@1
dec ax
@1:
    cmp ax,0                                    ;检查是否还有完整的扇区
    jz direct                                   ;为0，没有完整的扇区，跳转到direct
    push ds                                     ;不为0.表示还有没有读取完的扇区，保存ds的值，将其压栈
    mov cx,ax                                   ;把剩余的扇区数放入cx
@2:
    mov ax,ds                                   ;把当前ds指向的地址放入ax
    add ax,0x20                                 ;把ax + 0x20（512），这样就能指向下一个扇区
    mov ds,ax                                   ;把ax赋值给ds，这样ds就指向了下一个扇区的段地址

    xor bx,bx                                   ;把bx寄存器归0
    inc si                                      ;指向下一个硬盘扇区号
    call read_hard_disk_0                       ;指定扇区读取数据
    loop @2                                     ;反复执行

    pop ds                                      ;弹出ds寄存器（这个值指向用户程序的起始段地址（0x1000处））

direct:                                         ;为0，代表用户程序程序已经放入0x1000地址处完毕
    mov dx,[0x08]                               
    mov ax,[0x06]                               
    ;计算用户程序入口点代码段基址
    call calc_segment_base                      ;进入计算函数
    mov [0x06],ax                               ;把新的用户程序段的段基址，放入0x1000：0x06处

    mov cx,[0x0a]                               ;把用户程序表项个数放入cx寄存器
    mov bx,0x0c                                 ;把0x0c（12）放入bx

realloc:
    mov dx,[bx+0x02]
    mov ax,[bx]
    call calc_segment_base                      ;把用户程序的每一个程序段，都在引导扇区的内存里面重定位
    mov [bx],ax
    add bx,4
    loop realloc

    jmp far [0x04]                              ;远转移，开始执行用户程序

read_hard_disk_0:
    push ax
    push bx
    push cx
    push dx                                     ;将四个寄存器压栈，保护原值
    mov dx,0x01f2                               ;把硬盘端口0x01f2赋值给dx寄存器
    mov al,1                                    ;把要读取的扇区数量（1个）赋值给al寄存器
    out dx,al                                   ;把读一个扇区，告诉硬盘

                                                ;0x1f3~0x1f6端口，要知道LBA模式的扇区号
    inc dx                                      ;dx寄存器递增（0x1f3）
    mov ax,si                                   ;si寄存器放着用户所在的扇区100，赋值给ax
    out dx,al                                   ;把100扇区号所在的低八位，写入0x1f3端口

    inc dx                                      ;dx寄存器递增（0x1f4）
    mov al,ah                                   ;把100扇区号所在的高八位，放入al寄存器（为了输出到0x1f4）
    out dx,al                                   ;把100扇区号所在的高八位，写入0x1f4端口

    inc dx                                      ;dx寄存器递增（0x1f5）
    mov ax,di                                   ;di寄存器中的值是0
    out dx,al                                   ;把八位0写入0x1f5端口，因为100扇区只用一个八位就能表示，剩下的全是0即可
    
    inc dx                                      ;dx寄存器递增（0x1f6），目前已经写入了24位的LBA数据
    mov al,0xe0                                 ;0xe0，二进制是1110 0000， 写入al寄存器，111代表LBA模式，0代表主盘
    or al,ah                                    ; 把扇区号的高 4 位塞进 al 的低 4 位
    out dx,al                                   ;输出到0x1f6

    inc dx                                      ;dx寄存器递增（0x1f7）
    mov al,0x20                                 ;0x20,告诉要从硬盘读
    out dx,al                                   ;输出到0x1f7
.waits:
    in al,dx                                    ;现在dx指向0x1f7端口，这个端口提供数据是否准备好                                  
    and al,0x88                                 ;判断数据与上1000 1000，第一个1是判断硬盘忙不忙，第二个1是判断硬盘数据有没有准备好
    cmp al,0x08                                 ;判断硬盘数据有没有准备好（准备好了为0，没准备好不为0）
    jnz .waits                                  ;不为0，反复执行.waits代码

    mov cx,256                                  ;为0，跳转到这里，把256赋值给cx，一个扇区是512字节，一次能读2字节的数据，循环256次即可
    mov dx,0x1f0                                ;把0x1f0赋值给dx，这是从硬盘读数据的端口
.readw:
    in ax,dx                                    ;从硬盘读两个字节的数据，放入ax
    mov [bx],ax                                 ;把数据放入0x1000:bx处（为程序指定，因为用户程序要放入内存的这里）
    add bx,2                                    ;bx+2，以便下次数据读取
    loop .readw                                 ;反复执行，循环256次，读取一个扇区

    pop dx
    pop cx
    pop bx
    pop ax                                      ;寄存器还原初值

    ret                                         ;返回call调用

calc_segment_base:
    push dx                                     ;保存原先的dx值
    add ax,[cs:phy_base]                        ;ax += phy_base的低16位
    adc dx,[cs:phy_base+0x02]                   ;dx += phy_base的高16位
    shr ax,4
    ror dx,4
    and dx,0xf000                           
    or ax,dx                                    ;得到了用户程序代码段的起始地址，加载到0x10000处的，新的段基址
    pop dx                                      ;还原dx的值
    ret

phy_base dd 0x10000

times 510-($-$$) db 0
                 db 0x55,0xaa